#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/moduleparam.h>
#include <linux/types.h>
#include <linux/cdev.h>

#include "linux/gfp_types.h"
#include "linux/kobject.h"
#include "linux/semaphore.h"
#include "linux/slab.h"
#include "linux/sysfs.h"
#include "log.h"
#include "scull_common.h"

int scull_major = SCULL_MAJOR;
module_param(scull_major, uint, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
int scull_minor = SCULL_MINOR;
module_param(scull_minor, uint, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
int scull_dev_nr = SCULL_COUNT;
module_param(scull_dev_nr, uint, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
int scull_qset = SCULL_QSET;
module_param_named(qset, scull_qset, uint, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
int scull_quantum = SCULL_QUANTUM;
module_param_named(quantum, scull_quantum, uint, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));

int scull_trim(struct scull_dev *dev)
{
	struct scull_qset *p = NULL, *t;
	int i;

	LOGI("%s enter\n", __FUNCTION__);
	dev->length = 0;
	dev->data = NULL;

	// free data memory
	while (p) {
		for (i = 0; i < dev->qset_size; i++) {
			if (p->data[i]) {
				kfree(p->data[i]);
				p->data[i] = NULL;
			}
		}
		kfree(p->data);
		t = p->next;
		kfree(p);
		p = t;
	}

	LOGI("%s exit\n", __FUNCTION__);
	return 0;
}

static struct scull_qset *scull_follow(struct scull_qset *qset, int item)
{
	LOGI("%s enter\n", __FUNCTION__);

	while (qset && item) {
		qset = qset->next;
		item--;
	}

	return qset;
}

struct scull_qset *scull_qset_alloc(int quantum)
{
	struct scull_qset *pqset;

	pqset = (struct scull_qset*)kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
	if (pqset == NULL) {
		return NULL;
	}
	memset((void*)pqset, 0, sizeof(struct scull_qset));

	pqset->data = kmalloc(quantum * sizeof(void*), GFP_KERNEL);
	if (!pqset->data) {
		kfree(pqset);
		return NULL;
	}
	memset((void*)pqset->data, 0, quantum * sizeof(void*));
	return pqset;
}

int scull_open(struct inode *inode, struct file *filp)
{
	struct scull_dev *sdev = NULL;
	bool append = false;

	LOGI("%s enter\n", __FUNCTION__);

	sdev = container_of(inode->i_cdev, struct scull_dev, cdev);
	if (down_interruptible(&sdev->sem)) {
		return -ERESTARTSYS;
	}

	filp->private_data = sdev;

	append = !!(filp->f_flags & O_APPEND);
	if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {
		if (!append) {
			scull_trim(sdev);
		} else {
			LOGI("open to append");
			scull_llseek(filp, 0, SEEK_END);
		}
	}

	up(&sdev->sem);
	return 0;
}

ssize_t scull_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
	struct scull_dev *sdev;
	struct scull_qset *pqset;
	int qset, quantum;
	int item, item_size, q_idx, q_pos, rest;
	size_t count = 0;
	size_t batch;

	LOGI("%s enter\n", __FUNCTION__);
	LOGI("read %ld chars from %lld", size, *offset);
	sdev = (struct scull_dev*)filp->private_data;

	if (down_interruptible(&sdev->sem)) {
		return -ERESTARTSYS;
	}

	qset = sdev->qset_size;
	quantum = sdev->quantum_size;
	item_size = qset * quantum;

	// End of file
	if (*offset >= sdev->length) {
		goto out;
	}
	if (*offset + size > sdev->length) {
		size = sdev->length - *offset;
	}

	item = *offset / item_size;
	rest = *offset % item_size;
	q_idx = rest / quantum;
	q_pos = rest % quantum;

	pqset = scull_follow(sdev->data, item);
	LOGI("travel to target qset, index: %d", item);

	while (size) {
		if (pqset == NULL || pqset->data == NULL || pqset->data[q_idx] == NULL) {
			LOGI("reach eof, qset %d quantum %d pos %d", item, q_idx, q_pos);
			goto eof;
		}
		batch = q_pos + size > quantum ? quantum - q_pos : size;
		if (copy_to_user(buf+count, pqset->data[q_idx]+q_pos, batch)) {
			count = -EFAULT;
			goto eof;
		}
		size -= batch;
		count += batch;
		LOGI("read %ld chars from qset %d quantum %d at pos %d, total read %ld",
				batch, item, q_idx, q_pos, count);
		// update position, if current quantum is finished, move to next quantum
		q_pos = (q_pos+batch) % quantum;
		if (q_pos == 0) {
			q_idx++;
		}
		// check if current qset is finished
		if (q_idx >= qset) {
			pqset = pqset->next;
			q_idx = 0;
			item++;
			LOGI("tranvel to next qset, index: %d", item);
		}
	}

eof:
	*offset += count;
	LOGI("current file pos: %lld", *offset);
	LOGI("current file length: %ld", sdev->length);

out:
	up(&sdev->sem);
	return count;
}

ssize_t scull_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
	struct scull_dev *sdev;
	struct scull_qset *pqset, *preqset;
	int qset, quantum;
	int item, item_size, q_idx, q_pos, rest, i;
	int batch;
	ssize_t count;
	char *obuf;

	LOGI("%s enter\n", __FUNCTION__);
	LOGI("write %ld chars to %lld", size, *offset);
	sdev = (struct scull_dev*)filp->private_data;

	if (down_interruptible(&sdev->sem)) {
		return -ERESTARTSYS;
	}

	qset = sdev->qset_size;
	quantum = sdev->quantum_size;
	item_size = qset * quantum;
	item = *offset / item_size;
	rest = *offset % item_size;
	q_idx = rest / quantum;
	q_pos = rest % quantum;
	obuf = kmalloc(PAGE_SIZE, GFP_KERNEL);
	if (obuf == NULL) {
		count = -ENOMEM;
		goto out;
	}

	// 1. travel to the offset qset item and create all previous nodes
	pqset = sdev->data;
	preqset = NULL;
	i = -1;
	while (i < item) {
		i++;
		if (!pqset) {
			pqset = scull_qset_alloc(quantum);
			if (pqset == NULL) {
				LOGI("out of memory");
				count = -ENOMEM;
				goto err;
			}
			LOGI("alloc qset, index: %d", i);

			if (preqset == NULL) {
				sdev->data = pqset;
			} else {
				preqset->next = pqset;
			}
		}
		preqset = pqset;
		pqset = pqset->next;
	}

	// 2. write size data from q_pos and alloc quantum if desired quantum is NULL
	pqset = preqset;
	count = 0;
	while (size) {
		batch = q_pos + size > quantum ? quantum - q_pos : size;
		if (pqset->data[q_idx] == NULL) {
			pqset->data[q_idx] = kmalloc(quantum, GFP_KERNEL);
			if (pqset->data[q_idx] == NULL) {
				count = -ENOMEM;
				goto err;
			}
			memset(pqset->data[q_idx], 0, quantum);
			LOGI("alloc quantum in qset %d, index: %d", i, q_idx);
		}
		if (copy_from_user(pqset->data[q_idx] + q_pos, buf + count, batch)) {
			count = -EFAULT;
			goto err;
		}
		size -= batch;
		count += batch;
		snprintf(obuf, batch+1, "%s", (char*)pqset->data[q_idx]+q_pos);
		LOGI("write %d char in qset %d quantum %d at pos %d, total wrote %ld: '%s'",
				batch, i, q_idx, q_pos, count, obuf);
		// update position, if current quantum is full, move to next quantum
		q_pos = (q_pos+batch) % quantum;
		if (q_pos == 0) {
			q_idx++;
		}
		// check if current qset is full
		if (q_idx >= qset) {
			// Current qset is full, travel to next
			preqset = pqset;
			pqset = pqset->next;
			i++;
			if (pqset == NULL) {
				pqset = scull_qset_alloc(quantum);
				if (pqset == NULL) {
					count = -ENOMEM;
					goto err;
				}
				preqset->next = pqset;
				LOGI("alloc qset, index: %d", i);
			}
			LOGI("tranvel to next qset, index: %d", i);
			q_idx = 0;
		}
	}

	// 3. return bytes wrote
	*offset += count;
	LOGI("current file pos: %lld", *offset);
	if (*offset > sdev->length) {
		sdev->length = *offset;
		LOGI("current file length: %ld", sdev->length);
	}

err:
	kfree(obuf);
	obuf = NULL;
out:
	up(&sdev->sem);
	return count;
}

loff_t scull_llseek(struct file *filp, loff_t offset, int whence)
{
	struct scull_dev *sdev = filp->private_data;
	loff_t newpos;

	LOGI("%s enter\n", __FUNCTION__);

	if (down_interruptible(&sdev->sem)) {
		return -ERESTARTSYS;
	}

	switch (whence) {
		case SEEK_SET:
			newpos = offset;
			break;
		case SEEK_CUR:
			newpos = filp->f_pos + offset;
			break;
		case SEEK_END:
			newpos = sdev->length + offset;
			break;
		default:
			newpos = -EINVAL;
	}

	if (newpos != -EINVAL) {
		filp->f_pos = newpos;
		LOGI("seek to pos %lld", newpos);
	}

	up(&sdev->sem);
	return newpos;
}

static ssize_t name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
	const char *name;
	name = kobject_name(kobj);
	(void)strcpy(buf, name);
	return strlen(buf);
}
static struct kobj_attribute name_attr = __ATTR_RO(name);

static ssize_t qset_show(struct kobject *obj, struct kobj_attribute *attr, char *buf)
{
	(void)sprintf(buf, "%d", scull_qset);
	return strlen(buf);
}
static struct kobj_attribute qset_attr = __ATTR_RO(qset);

static ssize_t quantum_show(struct kobject *obj, struct kobj_attribute *attr, char *buf)
{
	(void)sprintf(buf, "%d", scull_quantum);
	return strlen(buf);
}
static struct kobj_attribute quantum_attr = __ATTR_RO(quantum);

static const struct attribute *scull_attrs[] = {
	&name_attr.attr,
	&qset_attr.attr,
	&quantum_attr.attr,
	NULL,
};

// Setup scull devno region.
static int scull_setup_devno(dev_t *dev_first)
{
	int err = 0;

	if (scull_major) {
		*dev_first = MKDEV(scull_major, scull_minor);
		err = register_chrdev_region(*dev_first, scull_dev_nr, scull_name);
		if (err) {
			LOGE("register %d chrdev %s with major %d return %d\n",
					scull_dev_nr, scull_name, scull_major, err);
			return err;
		}
	} else {
		err = alloc_chrdev_region(dev_first, scull_minor, scull_dev_nr, scull_name);
		if (err) {
			LOGE("alloc %d chrdev %s return %d\n",
					scull_dev_nr, scull_name, err);
			return err;
		}
		scull_major = MAJOR(*dev_first);
	}
	LOGI("%s chrdev setup with major %d\n", scull_name, scull_major);
	return 0;
}

// Initialize and register scull device structure.
int scull_setup_cdev(struct scull_dev *dev, int index, const struct file_operations *fops)
{
	int err;
	dev_t devno;
	char buf[128];

	err = scull_setup_devno(&devno);
	if (err) {
		return err;
	}

	// initialize scull device
	cdev_init(&dev->cdev, fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = fops;
	dev->qset_size = scull_qset;
	dev->quantum_size = scull_quantum;
	sema_init(&dev->sem, 1);

	// setup sysfs
	snprintf(buf, 128, "%s%d", scull_name, index);
	dev->kobj = kobject_create_and_add(buf, NULL);
	if (!dev->kobj) {
		LOGE("create chrdev %s%d failed when add kobject", scull_name, index);
		return -ENOMEM;
	}
	err = sysfs_create_files(dev->kobj, scull_attrs);
	if (err) {
		LOGE("create chrdev %s%d failed when create sysfs files, error %d\n",
				scull_name, index, err);
		return err;
	}

	err = cdev_add(&dev->cdev, devno, 1);
	if (err) {
		LOGE("add chrdev %s%d failed, error %d\n", scull_name, index, err);
		return err;
	} else {
		format_dev_t(buf, devno);
		LOGI("add chrdev %s%d, devno %s\n", scull_name, index, buf);
	}
	return 0;
}

// Unregister and delete scull device.
void scull_delete_cdev(struct scull_dev *dev, int index)
{
	scull_trim(dev);
	sysfs_remove_files(dev->kobj, scull_attrs);
	kobject_put(dev->kobj);
	cdev_del(&dev->cdev);
	LOGI("delete chrdev %s%d\n", scull_name, index);
}